/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

package org.unidata.mdm.search.context;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.unidata.mdm.search.exception.SearchApplicationException;
import org.unidata.mdm.search.exception.SearchExceptionIds;
/**
 * Need for complex search over parent child relation, child - child relation.
 */
public class ComplexSearchRequestContext {
    /**
     * Type of complex request.
     */
    public enum ComplexSearchRequestType {
        /**
         * Just a serie of independent requests (and responses).
         */
        MULTI,
        /**
         * Hierarhical (parent <-> child) request.
         */
        HIERARCHICAL;
    }
    /**
     * Main request
     */
    private final SearchRequestContext main;
    /**
     * Supplementary requests.
     */
    private final Collection<SearchRequestContext> supplementary;
    /**
     * An alternative way to supply nested contexts after target context has already been built.
     */
    private final Map<SearchRequestContext, Collection<NestedSearchRequestContext>> nested;
    /**
     * This context type.
     */
    private final ComplexSearchRequestType type;
    /**
     * private constructor
     */
    private ComplexSearchRequestContext(ComplexSearchRequestContextBuilder b) {
        super();
        this.type = b.type;
        this.main = b.main;
        this.supplementary = b.supplementary;
        this.nested = b.nested;
    }
    /**
     * @return set of unique entity names for requests
     */
    public Set<String> getEntityNames() {
        if (getType() == ComplexSearchRequestType.HIERARCHICAL) {
            return Collections.singleton(main.getEntity());
        } else {
            return getSupplementary().stream().map(SearchRequestContext::getEntity).collect(Collectors.toSet());
        }
    }

    /**
     * @return collection of search requests.
     */
    public Collection<SearchRequestContext> getAllContexts() {

        if (getType() == ComplexSearchRequestType.MULTI) {
            return getSupplementary();
        } else {
            List<SearchRequestContext> result = new ArrayList<>(getSupplementary());
            result.add(main);
            return result;
        }
    }

    /**
     * @return true if context doesn't contain any inner contexts.
     */
    public boolean isEmpty() {
        if (getType() == ComplexSearchRequestType.HIERARCHICAL) {
            return main == null;
        } else {
            return CollectionUtils.isEmpty(getSupplementary());
        }
    }
    /**
     * @return type of complex search request
     */
    public ComplexSearchRequestType getType() {
        return type;
    }
    /**
     * @return main request
     */
    @Nullable
    public SearchRequestContext getMain() {
        return main;
    }
    /**
     * @return supplementary requests
     */
    @Nonnull
    public Collection<SearchRequestContext> getSupplementary() {
        return Objects.isNull(supplementary) ? Collections.emptyList() : supplementary;
    }
    /**
     * Gets nested contexts collection for the given top type context.
     * @param ctx the top context
     * @return nested contexts collection for the given top type context
     */
    @Nonnull
    public Collection<NestedSearchRequestContext> getNested(SearchRequestContext ctx) {
        if (MapUtils.isNotEmpty(nested)) {
            Collection<NestedSearchRequestContext> n = nested.get(ctx);
            return Objects.isNull(n) ? Collections.emptyList() : n;
        }
        return Collections.emptyList();
    }
    /**
     * @param main          - main search request which results will be filtered by supplementary requests
     * @param supplementary - supplementary requests will be used for filtering results of main request.
     * @return complex search context for hierarchical search
     */
    @Nonnull
    public static ComplexSearchRequestContext hierarchical(@Nonnull SearchRequestContext main, Collection<SearchRequestContext> supplementary) {
        ComplexSearchRequestContextBuilder context = new ComplexSearchRequestContextBuilder(main);
        context.supplementary(supplementary);
        return context.build();
    }
    /**
     * @param main          - main search request which results will be filtered by supplementary requests
     * @param supplementary - supplementary requests will be used for filtering results of main request.
     * @return complex search context for hierarchical search
     */
    @Nonnull
    public static ComplexSearchRequestContext hierarchical(@Nonnull SearchRequestContext main, SearchRequestContext... supplementary) {
        return hierarchical(main, ArrayUtils.isEmpty(supplementary) ? Collections.emptyList() : Arrays.asList(supplementary));
    }
    /**
     * @param crossRequests - collection of search indexes.
     * @return complex request for searching over a few indexs.
     */
    @Nonnull
    public static ComplexSearchRequestContext multi(@Nonnull Collection<SearchRequestContext> crossRequests) {
        ComplexSearchRequestContextBuilder builder = new ComplexSearchRequestContextBuilder();
        builder.supplementary(crossRequests);
        return builder.build();
    }
    /**
     * @param crossRequests - collection of search indexes.
     * @return complex request for searching over a few indexs.
     */
    @Nonnull
    public static ComplexSearchRequestContext multi(@Nonnull SearchRequestContext required, SearchRequestContext... crossRequests) {
        ComplexSearchRequestContextBuilder builder = new ComplexSearchRequestContextBuilder();
        builder.supplementary(crossRequests);
        builder.supplementary(required);
        return builder.build();
    }
    /**
     * This variant builds a context of {@link ComplexSearchRequestType#HIERARCHICAL} type.
     * @param main the main simple search context.
     * @return builder
     */
    public static ComplexSearchRequestContextBuilder builder(SearchRequestContext main) {
        return new ComplexSearchRequestContextBuilder(main);
    }
    /**
     * This variant builds a context of {@link ComplexSearchRequestType#MULTI} type.
     * @return builder
     */
    public static ComplexSearchRequestContextBuilder builder() {
        return new ComplexSearchRequestContextBuilder();
    }
    /**
     * Simple builder class.
     */
    public static class ComplexSearchRequestContextBuilder implements SearchInputCollector {
        /**
         * Main request.
         */
        private SearchRequestContext main;
        /**
         * Supplementary requests
         */
        private Collection<SearchRequestContext> supplementary;
        /**
         * An alternative way to supply nested contexts after target context has already been built.
         */
        private Map<SearchRequestContext, Collection<NestedSearchRequestContext>> nested;
        /**
         * This request type.
         */
        private ComplexSearchRequestType type;
        /**
         * Hierarhical constructor.
         */
        private ComplexSearchRequestContextBuilder(SearchRequestContext main) {
            super();
            main(main);
        }
        /**
         * Multi constructor.
         */
        private ComplexSearchRequestContextBuilder() {
            super();
            this.type = ComplexSearchRequestType.MULTI;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public SearchInputCollectorType getCollectorType() {
            return SearchInputCollectorType.COMPLEX;
        }
        /**
         * Gets complex request type.
         * @return complex request type
         */
        public ComplexSearchRequestType getBuilderType() {
            return type;
        }
        /**
         * Sets the main search context.
         * @param ctx the context
         * @return self
         */
        public ComplexSearchRequestContextBuilder main(SearchRequestContext ctx) {

            Objects.requireNonNull(ctx, "Main hierarhical context must not be null");

            this.main = ctx;
            this.type = ComplexSearchRequestType.HIERARCHICAL;

            nested(main, main.getNestedSearch());
            return this;
        }
        /**
         * Adds supplementary contexts.
         * @param ctx the contexts
         * @return self
         */
        public ComplexSearchRequestContextBuilder supplementary(SearchRequestContext... ctx) {

            if (ArrayUtils.isNotEmpty(ctx)) {
                return supplementary(Arrays.asList(ctx));
            }

            return this;
        }
        /**
         * Adds supplementary contexts.
         * @param ctx the contexts
         * @return self
         */
        public ComplexSearchRequestContextBuilder supplementary(Collection<SearchRequestContext> ctx) {

            if (CollectionUtils.isNotEmpty(ctx)) {
                ctx.forEach(this::supplementary);
            }

            return this;
        }
        /**
         * Adds supplementary context.
         * @param ctx the context
         * @return self
         */
        public ComplexSearchRequestContextBuilder supplementary(SearchRequestContext ctx) {

            if (Objects.nonNull(ctx)) {

                if (supplementary == null) {
                    supplementary = new ArrayList<>();
                }

                supplementary.add(ctx);

                nested(ctx, ctx.getNestedSearch());
            }

            return this;
        }
        /**
         * Adds nested context.
         * @param ctx the context to perform nested search on
         * @param n nested context(s) to add
         * @return self
         */
        public ComplexSearchRequestContextBuilder nested(SearchRequestContext ctx, NestedSearchRequestContext... n) {

            if (Objects.nonNull(ctx) && ArrayUtils.isNotEmpty(n)) {
                return nested(ctx, Arrays.asList(n));
            }

            return this;
        }
        /**
         * Adds nested context.
         * @param ctx the context to perform nested search on
         * @param n nested context(s) to add
         * @return self
         */
        public ComplexSearchRequestContextBuilder nested(SearchRequestContext ctx, Collection<NestedSearchRequestContext> n) {

            if (Objects.nonNull(ctx) && CollectionUtils.isNotEmpty(n)) {

                if (nested == null) {
                    nested = new IdentityHashMap<>();
                }

                nested.computeIfAbsent(ctx, k -> new ArrayList<>(4))
                      .addAll(n);
            }

            return this;
        }
        /**
         * Throw 'validation failed'.
         * @param message the message to use
         */
        private static void throwInvalidInput(String message) {
            throw new SearchApplicationException(message, SearchExceptionIds.EX_SEARCH_COMPLEX_CONTEXT_INVALID_INPUT);
        }
        /**
         * The builder method.
         * @return new context
         */
        public ComplexSearchRequestContext build() {

            // Check co-working ability of supplementary contexts
            if (type == ComplexSearchRequestType.HIERARCHICAL && CollectionUtils.isNotEmpty(supplementary)) {

                String entity = main.getEntity();
                boolean isTheSameEntity = supplementary.stream().allMatch(ctx -> Objects.equals(ctx.getEntity(), entity));
                if (!isTheSameEntity) {
                    throwInvalidInput("Supplementary search contexts are NOT from the same index [entity].");
                }

                boolean isRelatedTypes = supplementary.stream().allMatch(ctx -> ctx.getType().isRelated(main.getType()));
                if (!isRelatedTypes) {
                    throwInvalidInput("Supplementary search contexts refer some unrelated types.");
                }
            }

            return new ComplexSearchRequestContext(this);
        }
    }
}
